Unit test是軟體開發不可缺少的一部分,通常一個功能的完成要伴隨相對應的unit test,它可以提升開發時的速度,結合自動化流程讓我們不用一直手動測試相同的地方。今天我想介紹在Redux專案中testing的方式,首先要先來簡單講一下Mocha。
Mocha是很紅的javascript測試框架,一般來說mocha的架構如下:
describe('Test suite description', () => {
it('Test case description', () => {
// assertion
expect(someFunc()).to.be.equal(1);
});
});
Test suite就是describe這個區塊,表示一組相關功能的測試項目,第一個參數傳入這組相關功能的描述,第二個參數是一個function,裡面包含一個或多個Test case,甚至也可以包含其他Test suite。
Test case就是it這個區塊,表示一個測試項目,第一個參數一樣傳入測試項目的描述,第二個參數也是一個function,裡面驗證某個功能,可以包含多個assertion。
Assertion就是expect這一行,它是斷言的意思,用來判斷執行的結果是否如預期。如果不如預期,這個測試項目就會fail。Mocha本身沒有包含assertion功能,通常我們需要再引用其他的assertion library,例如:chai、expect.js、should.js。之後範例會使用chai,以下先簡單介紹。
它裡面有三種style,should、expect、assert,在官網 Styles上可以直接看到這三種風格寫法,我覺得都滿好懂的,都是看指令就可以知道斷言的內容,非常口語化,可以挑一種你覺得最投緣的寫法XD。
接下來我會使用expect style,expect和should都屬於BDD style,它們初始化方式不同但使用方式類似,都有Language Chains提供一些口語的字來幫助組合斷言,例如:to、be、is、has...等,這些字只是用來增加測試的可讀性,並沒有判斷的功能。以下說明幾個有判斷功能的API。
/* 相等,equal是嚴格等於 === */
// 字串與數值的相等
expect('hello').to.equal('hello');
expect(42).to.equal(42);
// 比對值的相等,deep comparison
expect({ foo: 'bar'}).to.deep.equal({foo: 'bar'});
// boolean值的相等
expect(true).to.be.true;
/* 不相等,使用not來否定 === */
// 因為equal是嚴格等於
expect(1).to.not.equal(true);
// 因為不同object不相等,除非加deep
expect({ foo: 'bar'}).to.not.equal({ foo: 'bar'});
// boolean值為否定
expect(false).to.not.be.ok;
/* 其他比較方式 */
// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
// include
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');
// empty,長度為0,或是
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;
// NaN
expect('foo').to.be.NaN;
expect(4).not.to.be.NaN;
// match
expect('foobar').to.match(/^foo/);
// hasOwnProperty
expect('test').to.have.ownProperty('length');
是不是一看就知道要判斷什麼內容呢?Chai提供許多口語化的判斷的方式,可以視情況需要使用,盡量用容易明白的方式來撰寫斷言,官網有更詳細的API介紹。
如同前面提到,我們必須先安裝mocha、chai這兩個packages:
npm install mocha chai --save-dev
我們先設定mocha的指令在 package.json 的script中:
--recursive
讓mocha每一層資料夾內的檔案都執行。"scripts": {
// ...
"test": "mocha --compilers js:babel-core/register --recursive"
},
然後,設定 .babelrc 讓babel-core/register知道要編譯ES6:
{
"presets": ["es2015"]
}
如果mocha的指令太長,也可以在test目錄下建立 mocha.opts ,來管理mocha相關的指令,例如:
--compilers js:babel-core/register
--recursive
在Redux中我們針對action、reducer、component都有相對應的測試,測試的檔名通常會用原檔名,後面可以加.test.js
或者.spec.js
,.test.js
結尾通常表示測試,.spec.js
結尾通常表示規格,在我們的test中可以看個人習慣決定要使用哪一個結尾,後面範例我會使用.test.js
。
在redux中action是plain object,透過action creator回傳action,所以我們針對action creator來寫test,判斷它回傳的action是否正確。以下使用我們filter action來做範例。
test/actions/filter.test.js :
import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as filter from '../../src/actions/filter';
describe('filter action testing', () => {
it('should create an action to set filter', () => {
expect(
filter.setFilter('SHOW_ALL')
).to.be.deep.equal(
{
type: types.SET_FILTER,
filter: 'SHOW_ALL'
}
);
});
});
透過前面介紹的mocha寫法與chai的斷言,這樣的test很容易可以了解。不過,如果是非同步的action creator,我們還需要加上幾個步驟來撰寫測試。
因為我們是透過redux-thunk這個middleware來達成非同步的功能,所以在test時,要能夠mock store把middleware加進去,這邊我們透過redux-mock-store,來幫助我們mock store。
先安裝package:
npm install redux-mock-store --save-dev
這邊使用todos action的addTask
為範例:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as todos from '../../src/actions/todos';
// 建立mock store
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('todos action testing', () => {
it('should create an action to add task', () => {
const task = 'Learn mocha';
const expectedActions = [
{ type: types.ADD_TASK_REQUEST },
{ type: types.ADD_TASK_SUCCESS, task }
];
// mockStore裡面傳入會使用到的state,但這邊沒有所以放空的
const store = mockStore({});
// 模擬dispatch action
store.dispatch(todos.addTask(task));
// 原先範例是過一秒執行,這邊一秒後store.getActions()
setTimeout(() => {
expect(
store.getActions()
).to.be.deep.equal(expectedActions);
}, 1000);
});
});
透過mock store,我們把redux-thunk middleware加入,就可以模擬非同步的action。在實際上,通常會用到非同步是因為到後端fetch,我們可以透過nock來moch HTTP,以下是範例說明:
先安裝package:
npm install nock --save-dev
假設我們的action如下:
export function addTaskRequest(){
return {
type: 'ADD_TASK_REQUEST'
};
}
export function addTaskSuccess(task){
return {
type: 'ADD_TASK_SUCCESS',
task
};
}
export function addTaskFailure(err){
return {
type: 'ADD_TASK_FAILURE',
err
};
}
export function addTask(task){
return (dispatch) => {
dispatch(addTaskRequest());
return fetch('http://www.domain.com/saveData', {
method: 'POST',
body: JSON.stringify({
task
})
})
.then(response => {
dispatch(addTaskSuccess(task));
})
.catch(err => dispatch(addTaskFailure(err)));
};
}
action.test.js測試addTask
可以這樣寫:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock'; // 記得安裝nock package
import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as todos from '../../src/actions/todos';
// 建立mock store
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('todos action testing', () => {
afterEach(() => {
// 每個test case執行完,都做nock clean
nock.cleanAll();
});
it('should create an action to add task', () => {
const task = 'Learn mocha';
const expectedActions = [
{ type: types.ADD_TASK_REQUEST },
{ type: types.ADD_TASK_SUCCESS, task }
];
// mock HTTP連線
nock('http://www.domain.com')
.post('/saveData', JSON.stringify({ task }))
.reply(200);
const store = mockStore({});
return store.dispatch(todos.addTask(task))
.then(() => {
expect(
store.getActions()
).to.be.deep.equal(expectedActions);
});
});
});
上面是模擬post的情境,nock官網上有更多其他使用方式,也可以上去看更多喔!
今天我們已經介紹如何撰寫action test,我有把其他actions的unit test都補完,目前的code已經放到Git 上囉!